﻿using System;
using System.Text;

namespace Ndns.Packet.Abstractions
{
    internal class PunycodeEncoder : Encoder
    {
        private static PunycodeEncoder _instance = new PunycodeEncoder();
        public static PunycodeEncoder Default => _instance;

        public override int GetByteCount(char[] chars, int index, int count, bool flush)
        {
            int n = Punycode.INITIAL_N;
            int delta = 0;
            int bias = Punycode.INITIAL_BIAS;
            int len = 0;
            // Copy all basic code points to the output
            int b = 0;
            for (int i = 0; i < count; i++)
            {
                char c = chars[index + i];
                if (Punycode.IsBasic(c))
                {
                    len++;
                    b++;
                }
            }
            // Append delimiter
            if (b > 0)
            {
                len++;
            }
            int h = b;
            while (h < count)
            {
                int m = int.MaxValue;
                // Find the minimum code point >= n
                for (int i = 0; i < count; i++)
                {
                    int c = chars[index + i];
                    if (c >= n && c < m)
                    {
                        m = c;
                    }
                }
                if (m - n > (int.MaxValue - delta) / (h + 1))
                {
                    throw new OverflowException();
                }
                delta = delta + (m - n) * (h + 1);
                n = m;
                for (int j = 0; j < count; j++)
                {
                    int c = chars[index + j];
                    if (c < n)
                    {
                        delta++;
                        if (0 == delta)
                        {
                            throw new OverflowException();
                        }
                    }
                    if (c == n)
                    {
                        int q = delta;
                        for (int k = Punycode.BASE; ; k += Punycode.BASE)
                        {
                            int t;
                            if (k <= bias)
                            {
                                t = Punycode.TMIN;
                            }
                            else if (k >= bias + Punycode.TMAX)
                            {
                                t = Punycode.TMAX;
                            }
                            else
                            {
                                t = k - bias;
                            }
                            if (q < t)
                            {
                                break;
                            }
                            len++;
                            q = (q - t) / (Punycode.BASE - t);
                        }
                        len++;
                        bias = Punycode.Adapt(delta, h + 1, h == b);
                        delta = 0;
                        h++;
                    }
                }
                delta++;
                n++;
            }
            return len;
        }

        /// <inherentdoc />
        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, bool flush)
        {
            int n = Punycode.INITIAL_N;
            int delta = 0;
            int bias = Punycode.INITIAL_BIAS;
            int len = 0;
            // Copy all basic code points to the output
            int b = 0;
            for (int i = 0; i < charCount; i++)
            {
                char c = chars[charIndex + i];
                if (Punycode.IsBasic(c))
                {
                    bytes[byteIndex + len++] = (byte)c;
                    b++;
                }
            }
            // Append delimiter
            if (b > 0)
            {
                bytes[byteIndex + len++] = (byte)Punycode.DELIMITER;
            }
            int h = b;
            while (h < charCount)
            {
                int m = int.MaxValue;
                // Find the minimum code point >= n
                for (int i = 0; i < charCount; i++)
                {
                    int c = chars[charIndex + i];
                    if (c >= n && c < m)
                    {
                        m = c;
                    }
                }
                if (m - n > (int.MaxValue - delta) / (h + 1))
                {
                    throw new OverflowException();
                }
                delta = delta + (m - n) * (h + 1);
                n = m;
                for (int j = 0; j < charCount; j++)
                {
                    int c = chars[charIndex + j];
                    if (c < n)
                    {
                        delta++;
                        if (0 == delta)
                        {
                            throw new OverflowException();
                        }
                    }
                    if (c == n)
                    {
                        int q = delta;
                        for (int k = Punycode.BASE; ; k += Punycode.BASE)
                        {
                            int t;
                            if (k <= bias)
                            {
                                t = Punycode.TMIN;
                            }
                            else if (k >= bias + Punycode.TMAX)
                            {
                                t = Punycode.TMAX;
                            }
                            else
                            {
                                t = k - bias;
                            }
                            if (q < t)
                            {
                                break;
                            }
                            bytes[byteIndex + len++] = (byte)(Punycode.DigitToCodepoint(t + (q - t) % (Punycode.BASE - t)));
                            q = (q - t) / (Punycode.BASE - t);
                        }
                        bytes[byteIndex + len++] = (byte)Punycode.DigitToCodepoint(q);
                        bias = Punycode.Adapt(delta, h + 1, h == b);
                        delta = 0;
                        h++;
                    }
                }
                delta++;
                n++;
            }
            return len;
        }
    }
}
